package game;
import java.util.*;

import userInterface.CollisionListener;

import be.kuleuven.cs.som.annotate.*;

/**
 * A class of worlds involving a width, a height and a variable number of spaceObjects.
 * @invar	| hasProperSpaceObjects()
 * @invar	| isValidHeight(getHeight())
 * @invar	| isValidWidth(getWidth())
 * @author JeroenGillis && ThomasProvoost
 */
public class World {
		
	private final double width;
	private final double height;
	private static final double maxWidth = Double.MAX_VALUE;
	private static final double maxHeight = Double.MAX_VALUE;
	private final List<SpaceObject> objects = new ArrayList<SpaceObject>();
	
	/**
	 * Create a new default world. The width and height are 1000, the world has no space objects.
	 */
	public World() {
		this(1000,1000);		
	}
	
	/**
	 * Create a new world with given width and height.
	 * @param 	width
	 * 			The width of this new world.
	 * @param 	height
	 * 			The height of this new world.
	 * @post	| (new this).getWidth() == width
	 * @post	| (new this).getHeight() == height
	 * @throws	IllegalArgumentException
	 * 			| ! isValidWidth(width) || ! isValidHeight(height)
	 */
	public World(double width, double height) throws IllegalArgumentException {
		if(! isValidWidth(width) || ! isValidHeight(height)) 
			throw new IllegalArgumentException();
		this.width = width;
		this.height = height;
	}
	
	/**
	 * Return the spaceObject associated with this world at the
	 * given index. 
	 * @param  	index
	 *         	The index of the spaceObject to return.
	 * @throws 	IndexOutOfBoundsException         
	 *			| if( (index < 1) || (index > getNbSpaceObjects()) )
	 */
	@Basic
	public SpaceObject getSpaceObjectAt(int index) throws IndexOutOfBoundsException {
		return objects.get(index -1);
	}
	
	/** 
	 * Return the number of bullet associated with this ship.
	 */
	@Basic
	public int getNbSpaceObjects() {
		return objects.size();
	}
	
	/**
	 * Return a list of all the spaceObjects associated with this world.
	 */
	public List<SpaceObject> getAllSpaceObjects() {
		return objects;
	}
	
	/**
	 * Add the given spaceObject to the list of objects if the given spaceObject is a valid spaceObject for this world.
	 * @param	object
	 * 			The given object to be added.
	 * @post	| if(canHaveAsSpaceObject(object)) (new this).containsSpaceObject(object) == true
	 * @throws	IllegalArgumentException
	 * 			| if( (!canHaveAsSpaceObject(object)) || containsSpaceObject(object) )
	 */
	public void addSpaceObject(SpaceObject object) throws IllegalArgumentException {
		if( ! canHaveAsSpaceObject(object)) 
			throw new IllegalArgumentException();
		else if(containsSpaceObject(object))	
			throw new IllegalArgumentException();
		object.setWorld(this);
		getAllSpaceObjects().add(object);
	}
	
	/**
	 * Remove the given spaceObject from the list of objects if the list of objects of this world contains the spaceObject.
	 * @param 	object
	 * 			The object to be removed.
	 * @post	| (new this).containsSpaceObject(object) == false
	 * @throws	IllegalArgumentException
	 * 			| ! containsSpaceObject(object)
	 */
	public void removeSpaceObject(SpaceObject object) throws IllegalArgumentException {
		if(containsSpaceObject(object))
			objects.remove(object);
		else 
			throw new IllegalArgumentException();
	}
	
	/**
	 * Return a boolean reflecting whether this world can have the given spaceObject as an object.
	 * @param 	object
	 * 			The spaceObject to be checked.
	 * @return	| if(object == null || object.isTerminated() ) result == false
	 * 			| else result == object.canHaveAsWorld(this)
	 */
	public boolean canHaveAsSpaceObject(SpaceObject object) {
		if(object == null || object.isTerminated() ) return false;
		return object.canHaveAsWorld(this);
	}
	
	/**
	 * Return a boolean reflecting whether this world has valid space objects.
	 * @return	| if ( for each I in 1..getNbSpaceObjects(): 
	 * 			| 	(canHaveAsSpaceObject(getSpaceObjectAt(I)) &&  getSpaceObjectAt(I).getWorld() == this
	 * 			|	&& getSpaceObjectAt(I).isInWorldBoundaries()) ) &&
	 * 			| ( for each I,J in 1..getNbSpaceObjects(): getSpaceObjectAt(I).overlap(getSpaceObjectAt(J) == false || I == J ) 	result == true
	 * 			| else result == false
	 */
	public boolean hasProperSpaceObjects() {
		for(SpaceObject object : getAllSpaceObjects()) {
			if(!canHaveAsSpaceObject(object) || object.getWorld() != this || !object.isInWorldBoundaries()) return false;
			for(SpaceObject otherObject : getAllSpaceObjects())
				if(object.overlap(otherObject) && object != otherObject) return false;
		}
		return true;
	}
	
	/**
	 * Return a boolean reflecting whether the list of space objects of this world contains the given object.
	 * @param 	object
	 * 			The object to be checked.
	 * @return	| result == objects.contains(object)
	 */
	public boolean containsSpaceObject(SpaceObject object) {
		return objects.contains(object);
	}

	/**
	 * Return the final width of this world.
	 */
	@Basic
	public double getWidth() {
		return width;
	}

	/**
	 * Return the final height of this world.
	 */
	@Basic
	public double getHeight() {
		return height;
	}
	
	/**
	 * Return the final maximum width of the class world.
	 */
	@Basic
	public static double getMaxWidth() {
		return maxWidth;
	}

	/**
	 * Return the final maximum height of the class world.
	 */
	@Basic
	public static double getMaxHeight() {
		return maxHeight;
	}

	/**
	 * Return a boolean reflecting whether the given height is a valid height for this world.
	 * @param 	height
	 * 			The height to be checked.
	 * @return	| if ( 0 <= height <= Double.MAX_VALUE ) result == true
	 * 			| else result == false
	 */
	public boolean isValidHeight(double height) {
		return (0 <= height && height <= getMaxHeight());
	}
	
	/**
	 * Return a boolean reflecting whether the given width is a valid width for this world.
	 * @param 	width
	 * 			The width to be checked.
	 * @return	| if ( 0 <= width <= Double.MAX_VALUE ) result == true
	 * 			| else result == false
	 */
	public boolean isValidWidth(double width) {
		return (0 <= width && width <= getMaxWidth());
	}
	
	/**
	 * Return all the ships in this world.
	 * @return	| for each object in getAllSpaceObjects() 
	 * 			|	if(object instanceof Ship)  getShips().contains(object) == true
	 * 			|	if(!object instanceof Ship)  getShips().contains(object) == false
	 */
	public Set<Ship> getShips() {
		Set<Ship> ships = new HashSet<Ship>();
		for(SpaceObject object : getAllSpaceObjects()) {
			if(object instanceof Ship) 
				ships.add((Ship) object);
		}
		return ships;
	}

	/**
	 * Return all the asteroids in this world.
	 * @return	| for each object in getAllSpaceObjects() 
	 * 			|	if(object instanceof Asteroid)  getAsteroids().contains(object) == true
	 * 			|	if(!object instanceof Asteroid)  getAsteroids().contains(object) == false
	 */
	public Set<Asteroid> getAsteroids() {
		Set<Asteroid> asteroids = new HashSet<Asteroid>();
		for(SpaceObject object : getAllSpaceObjects()) {
			if(object instanceof Asteroid) 
				asteroids.add((Asteroid) object);
		}
		return asteroids;
	}

	/**
	 * Return all the bullets in this world.
	 * @return	| for each object in getAllSpaceObjects() 
	 * 			|	if(object instanceof Bullet)  getBullets().contains(object) == true
	 * 			|	if(!object instanceof Bullet)  getBullets().contains(object) == false
	 */
	public Set<Bullet> getBullets() {
		Set<Bullet> bullets = new HashSet<Bullet>();
		for(SpaceObject object : getAllSpaceObjects()) {
			if(object instanceof Bullet) 
				bullets.add((Bullet) object);
		}
		return bullets;
	}
	

	/**
	* A method to evolve this world with a given amount of time.
	* @param	dt
	* 			The amount of time to evolve this world.
	* @param 	collisionListener
	* 			The collisionListener used to resolve collisions.
	* 
	* 			| SpaceObject[] nextCollision = getNextCollision()
	* 
	*			| if(nextCollision[0] == null)
	* 			| timeToNextcollision = Double.POSITIVE_INFINITY
	* 			| if(nextCollision[1] == null)
	* 			| timeToNextCollision[1] = nextCollision[0].getTimeToBoundary()
	* 			| else timeToNextCollision = nextCollision[0].getTimeToCollision(nextCollision[1])
	* 
	* @post 	| if(Util.fuzzyEquals(dt, 0)
	* 			| new this = this
	* @effect 	| if(timeToNextCollision > dt)
	* 			| moveSpaceObjects(dt)
	* 			| thrustShips()
	* @effect 	| else
	* 			| moveSpaceObjects(timeToNextCollision)
	* 			| thrustShips()
	* 			| resolveCollision(nextCollision, collisionListener)
	* 			| evolve(dt - timeToNextCollision, collisionListener)
	*/
	public void evolve(double dt, CollisionListener collisionListener) {
		if(Util.fuzzyEquals(dt, 0))
			return;
		
		SpaceObject[] nextCollision = getNextCollision();
		double timeToNextCollision;
		if(nextCollision[0] == null) 
			timeToNextCollision = Double.POSITIVE_INFINITY;
		else if(nextCollision[1] == null)
			timeToNextCollision = nextCollision[0].getTimeToBoundary();
		else
			timeToNextCollision = nextCollision[0].getTimeToCollision(nextCollision[1]);

		if(timeToNextCollision > dt) {
			moveSpaceObjects(dt);
			thrustShips();
		}
		else 
		{
			moveSpaceObjects(timeToNextCollision);
			thrustShips();
			resolveCollision(nextCollision, collisionListener);
			evolve(dt - timeToNextCollision, collisionListener);
			} 
		}

	/**
	 * Move all space objects in this world.
	 * @param 	moveTime
	 * 			The time all space objects move.
	 * @effect	| for each object in getAllSpaceObjects() 
	 * 			|	object.move(moveTime)
	 */
	public void moveSpaceObjects(double moveTime) {
		for (SpaceObject object : getAllSpaceObjects()) {
			object.move(moveTime);
			}
	}
	
	/**
	 * Thrusts all ships in this world.
	 * @effect	| for each ship in getShips()
	 *			|		ship.thrust(time)
	 */
	public void thrustShips() {
		for(Ship ship : getShips()) {
			ship.thrust();
		}
	}
	

	/**
	 * Return a collection of SpaceObjects with their thruster active at the moment this method is invoked.
	 * @return	| for each ship in getShips(): 
	 * 			|	if(ship.isActiveThruster()) getThrustingShips().contains(ship) == true
	 * 			|	else getThrustingShips().contains(ship) == false
	 */
	public Collection<SpaceObject> getThrustingShips() {
		Collection<SpaceObject> thrustingShips = new ArrayList<SpaceObject>();
		for(Ship ship : getShips()) {
			if(ship.isActiveThruster())
				thrustingShips.add(ship);
		}
		return thrustingShips;
	}
	
	/**
	 * Resolve the collision between the given SpaceObjects.
	 * @param 	collision
	 * 			The space objects that collide.
	 * @effect	| if(collision[1] == null) collision[0].hitBoundary()
	 * 			| else  collision[0].collideWith(collision[1])
	 */
	public void resolveCollision(SpaceObject[] collision, CollisionListener collisionListener) throws IllegalStateException,IllegalArgumentException {
		if(collision[1] == null) {
			if( collisionListener != null)
				collisionListener.boundaryCollision(collision[0], collision[0].getxPosition(), collision[0].getyPosition());
			collision[0].hitBoundary();
		}
		else {
			if(collisionListener != null)
				collisionListener.objectCollision(collision[0], collision[1], collision[0].getCollisionPosition(collision[1])[0], collision[0].getCollisionPosition(collision[1])[1]);
			collision[0].collideWith(collision[1]);
		}
		
	}
	
	/**
	 * Return a array of the two space objects who will be the first of all space objects in this world to collide with each other.
	 * @return	| for each I,J from 0..getNbOfSpaceObjects()
	 * 			|	getObjectAt(I).getTimeToCollision(getObjectAt(J)) >= 
	 * 			|		getNextCollision()[0].getTimeToCollision(getNextCollision()[1])
	 * 			|	&& getObjectAt(I).getTimeToBoundary() >= getNextCollision()[0].getTimeToCollision(getNextCollision()[1]) 
	 */
	public SpaceObject[] getNextCollision() {
		Double smallestTimeToCollision = Double.POSITIVE_INFINITY;
		SpaceObject[] collidingSpaceObjects = new SpaceObject[2];
		for(SpaceObject object1 : getAllSpaceObjects()) {
			for(SpaceObject object2 : getAllSpaceObjects()) {
				double timeToCollision = object1.getTimeToCollision(object2);
				if(timeToCollision < smallestTimeToCollision) {
					smallestTimeToCollision = timeToCollision;
					collidingSpaceObjects[0] = object1;
					collidingSpaceObjects[1] = object2;
				}
			}
			if(object1.getTimeToBoundary() < smallestTimeToCollision) {
				smallestTimeToCollision = object1.getTimeToBoundary();
				collidingSpaceObjects[0] = object1;
				collidingSpaceObjects[1] = null;
			}	
		}
		return collidingSpaceObjects;			
	}
	
	public boolean overlaps(SpaceObject object) {
		if(this.containsSpaceObject(object))
			return true;
		for(SpaceObject objectInWorld : getAllSpaceObjects())
			if(objectInWorld.overlap(object)) {
				return true;
			}
		return false;
	}	
	
}
